package search_alg;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import city_model.City;
import city_model.Junction;
import city_model.Line;
import city_model.Station;
import data_reader.TimeReader;

public class AStarAlg
{
	private static final int walkingSpeed = 3;
	private static final int maximumWalkingDistanceFromStart = 500;
	private static final int maximumWalkingDistanceToStop    = 700;
	private static final long currentTime = System.currentTimeMillis();
	
	public ArrayList<RouteNodeInformation> AStarSearch(Station start, Station goal, City c, ArrayList<RouteEdgeAvailability> routeEdgeAvailability)
	{
		Map<String, Integer> walkingTimeFromStart = initWakingTimes(start, goal, c);
		Map<String, RouteNodeInformation> walkingTimeWithIntermediate = new HashMap<String,RouteNodeInformation>();	
		
		Map<RouteNodeInformation, String> path = new HashMap<RouteNodeInformation, String>();
		
		ArrayList<String> openSet = new ArrayList<String>();
		ArrayList<String> closedSet = new ArrayList<String>();
		
		openSet.add(start.getId());
		walkingTimeFromStart.put(start.getId(), 0);
		walkingTimeWithIntermediate.put(start.getId(), new RouteNodeInformation(start.getId(), walkingTimeFromStart.get(start.getId()) + 
				                                                                returnWalkingTime(start, goal), "w"));
		
		while (openSet.size() != 0)
		{			
			String currentId = returnLowestWalkingTime(walkingTimeWithIntermediate);
			RouteNodeInformation nodeInfo = walkingTimeWithIntermediate.get(currentId);
			
			if (currentId.equals(goal.getId()))
			{
				return reconstructPath(path, currentId);
			}
			openSet.remove(currentId);
			walkingTimeWithIntermediate.remove(currentId);
			closedSet.add(currentId);
			
			Map<String, RouteNodeInformation> neighborNodes = getNeighborNodes(c, start, goal, currentId, nodeInfo.getTransportMode(), routeEdgeAvailability);
			
			for (Map.Entry<String, RouteNodeInformation> entry :neighborNodes.entrySet())
			{ 
			   int tempWalkingTime = walkingTimeFromStart.get(currentId) + entry.getValue().getTime();
			   if (closedSet.contains(entry.getKey()))
			   {
				   if (tempWalkingTime >= walkingTimeFromStart.get(entry.getKey()))
				   {
					   continue;
				   }
			   }
			   
			   if (!openSet.contains(entry.getKey()) && (tempWalkingTime < walkingTimeFromStart.get(entry.getKey())))
			   {
				   RouteNodeInformation pathNodeInfo = new RouteNodeInformation(entry.getValue().getNodeId(),
						                                                        entry.getValue().getTime(),
						                                                        entry.getValue().getTransportMode());
				   path.put(pathNodeInfo, currentId);
				   
				   walkingTimeFromStart.put(entry.getKey(), tempWalkingTime);
				  
				   RouteNodeInformation nextNodeInfo = entry.getValue();
				   if (!entry.getKey().equals(goal.getId()))
				   {
				      nextNodeInfo.setTime(tempWalkingTime + returnWalkingTime(c.getStationById(entry.getKey()), goal));
				   }
				   else
				   {
					   nextNodeInfo.setTime(tempWalkingTime);
				   }				   
				   walkingTimeWithIntermediate.put(entry.getKey(), nextNodeInfo);
				  
				   if (!openSet.contains(entry.getKey()))
				   {
					   openSet.add(entry.getKey());
				   }
			   }
			}
			
		}
	return null;	
	}

	public int returnWalkingTime(Station a, Station b)
	{
		double result = 0;
		int distance = a.directDistanceTo(b);
		if (distance != -1)
		{
			result = ((double)((double)distance/1000)/walkingSpeed)*3600;
			return (int)result;
		}
		else
		{
			return -1;
		}	
	}
	
	public int returnDirectTime(Station a, Station b)
	{
		double result = 0;
		int distance = a.directDistanceTo(b);
		if (distance != -1)
		{
			result = ((double)((double)distance/1000)/walkingSpeed)*3600;
			return (int)result;
		}
		else
		{
			return -1;
		}	
	}
	
//	public Map<String, Integer> initWakingTimes(Station s, Station g, City c)
//	{
//		Map<String, Integer> times = new HashMap<String,Integer>();
//		
//		for (Station it : c.getStations())
//		{
//			if (returnWalkingTime(s, it) != -1)
//			{
//				times.put(it.getId(), returnWalkingTime(s, it)+1);
//			}
//		}
//		times.put(g.getId(),returnWalkingTime(s, g));	
//		return times;
//	}
	
	public Map<String, Integer> initWakingTimes(Station s, Station g, City c)
	{
		Map<String, Integer> times = new HashMap<String,Integer>();
		
		for (Station it : c.getStations())
		{
		    times.put(it.getId(), Integer.MAX_VALUE);
		}
		times.put(g.getId(),Integer.MAX_VALUE);	
		return times;
	}
	
	public Map<String, RouteNodeInformation> getNeighborsForStart(Station s, City c, ArrayList<RouteEdgeAvailability> routeEdgeAvailability)
	{
		Map<String, RouteNodeInformation> times = new HashMap<String,RouteNodeInformation>();
		
		for (Station it : c.getStationsInWalkingDistance(s, maximumWalkingDistanceFromStart))
		{
			int walkingTime = returnWalkingTime(s, it);
			if (walkingTime != -1)
			{
				if (isRouteEdgeAvailable(s.getId(), it.getId(), "w", routeEdgeAvailability))
				{
					times.put(it.getId(), new RouteNodeInformation(it.getId(), walkingTime, "w"));
				}
			}
		}
		return times;
	}
	
	public String returnLowestWalkingTime(Map<String, RouteNodeInformation> walkingTime)
	{
		String result = "";
		Integer min = Integer.MAX_VALUE;
		
		for (Map.Entry<String, RouteNodeInformation> entry :walkingTime.entrySet())
		{
			RouteNodeInformation i = entry.getValue();
			
			if (i.getTime() < min)
			{
				min = i.getTime();
				result = entry.getKey();
			}
		}
		return result;
	}
	
	public ArrayList<RouteNodeInformation> reconstructPath(Map<RouteNodeInformation, String> path, String goal)
	{
		ArrayList<RouteNodeInformation> result = new ArrayList<RouteNodeInformation>();
		if (getPathValue(path, goal) != null)
		{ 
			ArrayList<RouteNodeInformation> temp = reconstructPath(path,getPathValue(path, goal));
			temp.add(getPathKey(path, goal));
			return temp;
		}
		else
		{
			result.add(new RouteNodeInformation(goal, 0, "s"));
			return result;
		}
	}
	
	private String getPathValue(Map<RouteNodeInformation, String> path, String goalId)
	{
		for (Map.Entry<RouteNodeInformation, String> entry :path.entrySet())
		{
			RouteNodeInformation i = entry.getKey();
			if (i.getNodeId().equals(goalId))
			{
				return entry.getValue();
			}
		}
		return null;
	}
	
	private RouteNodeInformation getPathKey(Map<RouteNodeInformation, String> path, String goalId)
	{
		for (Map.Entry<RouteNodeInformation, String> entry :path.entrySet())
		{
			RouteNodeInformation i = entry.getKey();
			if (i.getNodeId().equals(goalId))
			{
				return i;
			}
		}
			return null;
	}
	
	
	public Map<String, RouteNodeInformation> getNeighborNodes(City city, Station start, Station goal, String current, String trMode, ArrayList<RouteEdgeAvailability> routeEdgeAvailability)
	{
		Station node = city.getStationById(current);
		Map<String, RouteNodeInformation> result = new HashMap<String, RouteNodeInformation>();
		
		if (node == null && current.equals(start.getId()))
		{
			result = getNeighborsForStart(start, city, routeEdgeAvailability);
		}
		else
		{
			result = returnTimesForNode(node, start, city, trMode, routeEdgeAvailability);
			if (city.getStationById(goal.getId()) == null)
			{
				if(city.getStationsInWalkingDistance(goal, maximumWalkingDistanceToStop).contains(node))
				{
					if (isRouteEdgeAvailable(current, goal.getId(), "w", routeEdgeAvailability))
					{
						result.put(goal.getId(), new RouteNodeInformation(goal.getId(), returnWalkingTime(node, goal), "w"));
					}
				}
			}
		}
		return result;
	}

	public Map<String, RouteNodeInformation> returnTimesForNode(Station station, Station start, City city, String trMode, ArrayList<RouteEdgeAvailability> routeEdgeAvailability)
	{
        Map<String, RouteNodeInformation> times = new HashMap<String,RouteNodeInformation>();
        
        Junction j = city.getJunctionByStation(station);
        if (j != null)
        {
        	for (String s : j.getStations())
        	{
        		int walkingTime = returnWalkingTime(station, city.getStationById(s));
        		if (walkingTime != -1 && walkingTime != 0 && !s.equals(start.getId()))
        		{
        			if (isRouteEdgeAvailable(station.getId(), s, "w", routeEdgeAvailability))
        			{
        				times.put(s, new RouteNodeInformation(s, returnWalkingTime(station, city.getStationById(s)), "w"));
        			}
        		}
        	}
        }
        
        List<String> nStations = city.getNextStationForEachLine(station);
        if (nStations != null)
        {
        	for (String nextStationId : nStations)
        	{
        		ArrayList<Calendar> waitingTime  = null;
        	    String     lineToNextStation = null;
        	    
        		List<Line> linesToNext = city.getLinesForStation(city.getStationById(nextStationId));
        		if (linesToNext != null)
        	    {
        	    	if(isLineInLines(linesToNext, trMode))
        	    	{
        	    		ArrayList<Calendar> timeCurrentStation = getTimeInformation(city, trMode, station.getId());
            			waitingTime = timeCurrentStation;
            			lineToNextStation = trMode;
        	    	}
        	    	else
        	    	{
        	    		double maxTime = Double.MAX_VALUE;
            	    	for(Line lineToNext: linesToNext)
                		{
            	    		ArrayList<Calendar> timeCurrentStation =getTimeInformation(city, lineToNext.getId(), station.getId());
                    		if (timeCurrentStation != null)
                    		{
                    			long time = timeCurrentStation.get(0).getTimeInMillis() - System.currentTimeMillis();
                    			if (time < maxTime)
                    			{
                    				maxTime = time;
                    				waitingTime = timeCurrentStation;
                    				lineToNextStation = lineToNext.getId();
                    			}
                    		}
                		}	
        	    	}
        	    }
        		
        		if (waitingTime != null && lineToNextStation != null && trMode != null)
        		{
        			ArrayList<Calendar> timeNextStation = getTimeInformation(city, lineToNextStation, nextStationId);
        			if (timeNextStation != null)
        			{
        				long ctime = currentTime;
        				long time0 = waitingTime.get(0).getTimeInMillis() - ctime;
        				long time1 = timeNextStation.get(0).getTimeInMillis() - ctime;
        				long time2 = timeNextStation.get(1).getTimeInMillis() - ctime;
        				double calculatedTime = 0;
        			
        				if(!trMode.equals(lineToNextStation))
        				{
        					calculatedTime += time0/1000;
        				}
        			
        				if (time0 < time1)
        				{
        					calculatedTime += (double)(time1-time0)/1000;
        				}
        				else
        				{
        					calculatedTime += (double)(time2-time0)/1000;
        				}
        				
        				if (isRouteEdgeAvailable(station.getId(), nextStationId, lineToNextStation, routeEdgeAvailability))
        				{
        					times.put(nextStationId, new RouteNodeInformation(nextStationId, (int)calculatedTime, lineToNextStation));
        				}
        			}
        		}
        	}
        }
	 return times;	
	}
	
	private boolean isLineInLines(List<Line> lines, String lineId)
	{
		boolean result = false;
		for(Line l : lines)
		{
			if (l.getId().equals(lineId))
			{
				result = true;
			}
		}
		return result;
	}
	
	private ArrayList<Calendar> getTimeInformation(City city, String lineId, String stationId)
	{
		Station station = city.getStationById(stationId);
		
		if(station.isTimeInformationSet(lineId))
		{
			return station.getTimeInformation(lineId);
		}
		else
		{
			ArrayList<Calendar> times = new TimeReader().getTimes(lineId, stationId);
			station.setTimeInformation(lineId, times);
			return times;
		}
		
	}
	
	private boolean isRouteEdgeAvailable(String fromStationId, String toStationId, String transportMode, ArrayList<RouteEdgeAvailability> routeEdgeAvailability)
	{
		for (RouteEdgeAvailability entry :routeEdgeAvailability)
		{
			if(entry.getFromStationId().equals(fromStationId) &&
			   entry.getToStationId().equals(toStationId)     &&
			   entry.getTransportMode().equals(transportMode)    )
			{
				return false;
			}		
		}
		return true;
	}

}
